Um guia para criar um indicador de percentual de conclusão de formulário em tempo real no React com o hook useFormStatus para uma UX superior.
Dominando a UX de Formulários: Construindo um Indicador Dinâmico de Percentual de Conclusão com o useFormStatus do React
No mundo do desenvolvimento web, os formulários são a interseção crítica onde usuários e aplicações trocam informações. Um formulário mal projetado pode ser um grande ponto de atrito, levando à frustração do usuário e a altas taxas de abandono. Por outro lado, um formulário bem elaborado parece intuitivo, útil e incentiva a conclusão. Uma das ferramentas mais eficazes em nosso kit de ferramentas de experiência do usuário (UX) para alcançar isso é um indicador de progresso em tempo real.
Este guia levará você a um mergulho profundo na criação de um indicador dinâmico de percentual de conclusão de formulário no React. Exploraremos como rastrear a entrada do usuário em tempo real e, crucialmente, como integrar isso com recursos modernos do React, como o hook useFormStatus, para fornecer uma experiência contínua desde a primeira tecla pressionada até o envio final. Esteja você construindo um simples formulário de contato ou um complexo processo de registro de várias etapas, os princípios abordados aqui ajudarão você a criar uma interface mais envolvente e amigável.
Entendendo os Conceitos Essenciais
Antes de começarmos a construir, é essencial entender os conceitos modernos do React que formam a base da nossa solução. O hook useFormStatus está intrinsecamente ligado aos React Server Components e Server Actions, uma mudança de paradigma na forma como lidamos com mutações de dados e comunicação com o servidor.
Uma Breve Visão sobre as React Server Actions
Tradicionalmente, o tratamento de envios de formulários no React envolvia JavaScript do lado do cliente. Escrevíamos um manipulador onSubmit, impedíamos o comportamento padrão do formulário, coletávamos os dados (frequentemente com useState) e, em seguida, fazíamos uma chamada de API usando fetch ou uma biblioteca como Axios. Esse padrão funciona, mas envolve muito código repetitivo.
Server Actions simplificam esse processo. Elas são funções que você pode definir no servidor (ou no cliente com a diretiva 'use server') e passar diretamente para a prop action de um formulário. Quando o formulário é enviado, o React lida automaticamente com a serialização dos dados e a chamada da API, executando a lógica do lado do servidor. Isso simplifica o código do lado do cliente e colocaliza a lógica de mutação com os componentes que a utilizam.
Apresentando o Hook useFormStatus
Quando o envio de um formulário está em andamento, você precisa de uma maneira de dar feedback ao usuário. A solicitação está sendo enviada? Foi bem-sucedida? Falhou? É precisamente para isso que serve o useFormStatus.
O hook useFormStatus fornece informações de status sobre o último envio de um <form> pai. Ele retorna um objeto com as seguintes propriedades:
pending: Um booleano que étrueenquanto o formulário está sendo ativamente enviado, efalsecaso contrário. Isso é perfeito para desabilitar botões ou mostrar spinners de carregamento.data: Um objetoFormDatacontendo os dados que foram enviados. Isso é incrivelmente útil para implementar atualizações de UI otimistas.method: Uma string que indica o método HTTP usado for o envio (ex: 'GET' ou 'POST').action: Uma referência à função que foi passada para a propactiondo formulário.
Regra Crucial: O hook useFormStatus deve ser usado dentro de um componente que seja descendente de um elemento <form>. Ele não pode ser usado no mesmo componente que renderiza a tag <form>; ele deve estar em um componente filho.
O Desafio: Conclusão em Tempo Real vs. Status de Envio
Aqui chegamos a uma distinção fundamental. O hook useFormStatus é brilhante para entender o que acontece durante e após o envio de um formulário. Ele informa se o formulário está 'pendente'.
No entanto, um indicador de percentual de conclusão de formulário é sobre o estado do formulário antes do envio. Ele responde à pergunta do usuário: "Quanto deste formulário eu já preenchi corretamente até agora?" Esta é uma preocupação do lado do cliente que precisa reagir a cada toque de tecla, clique ou seleção que o usuário faz.
Portanto, nossa solução será uma história de duas partes:
- Gerenciamento de Estado do Lado do Cliente: Usaremos hooks padrão do React como
useStateeuseMemopara rastrear os campos do formulário e calcular o percentual de conclusão em tempo real. - Gerenciamento de Estado de Envio: Em seguida, usaremos o
useFormStatuspara aprimorar a UX durante o processo de envio real, criando um ciclo de feedback completo e de ponta a ponta para o usuário.
Implementação Passo a Passo: Construindo o Componente da Barra de Progresso
Vamos ser práticos e construir um formulário de registro de usuário que inclua nome, e-mail, país e um acordo de termos de serviço. Adicionaremos uma barra de progresso que se atualiza conforme o usuário preenche esses campos.
Passo 1: Definindo a Estrutura e o Estado do Formulário
Primeiro, vamos configurar nosso componente principal com os campos do formulário e gerenciar seu estado usando useState. Este objeto de estado será a única fonte da verdade para os dados do nosso formulário.
// No seu arquivo de componente React, ex: RegistrationForm.js
'use client'; // Necessário para usar hooks como useState
import React, { useState, useMemo } from 'react';
const initialFormData = {
fullName: '',
email: '',
country: '',
agreedToTerms: false,
};
export default function RegistrationForm() {
const [formData, setFormData] = useState(initialFormData);
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prevData => ({
...prevData,
[name]: type === 'checkbox' ? checked : value,
}));
};
// ... a lógica de cálculo e o JSX virão aqui
return (
<form className="form-container">
<h2>Crie Sua Conta</h2>
{/* A Barra de Progresso será inserida aqui */}
<div className="form-group">
<label htmlFor="fullName">Nome Completo</label>
<input
type="text"
id="fullName"
name="fullName"
value={formData.fullName}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Endereço de E-mail</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="country">País</label>
<select
id="country"
name="country"
value={formData.country}
onChange={handleInputChange}
required
>
<option value="">Selecione um país</option>
<option value="USA">Estados Unidos</option>
<option value="CAN">Canadá</option>
<option value="GBR">Reino Unido</option>
<option value="AUS">Austrália</option>
<option value="IND">Índia</option>
</select>
</div>
<div className="form-group-checkbox">
<input
type="checkbox"
id="agreedToTerms"
name="agreedToTerms"
checked={formData.agreedToTerms}
onChange={handleInputChange}
required
/>
<label htmlFor="agreedToTerms">Eu concordo com os termos e condições</label>
</div>
{/* O Botão de Envio será adicionado mais tarde */}
</form>
);
}
Passo 2: A Lógica para Calcular o Percentual de Conclusão
Agora, a lógica principal. Precisamos definir o que "completo" significa para cada campo. Para o nosso formulário, as regras são:
- Nome Completo: Não deve estar vazio.
- Email: Deve ter um formato de e-mail válido (usaremos uma regex simples).
- País: Deve ter um valor selecionado (não ser uma string vazia).
- Termos: A caixa de seleção deve estar marcada.
Criaremos uma função para encapsular essa lógica e a envolveremos em useMemo. Esta é uma otimização de desempenho que garante que o cálculo só seja executado novamente quando o formData do qual depende tiver sido alterado.
// Dentro do componente RegistrationForm
const completionPercentage = useMemo(() => {
const fields = [
{
key: 'fullName',
isValid: (value) => value.trim() !== '',
},
{
key: 'email',
isValid: (value) => /^\S+@\S+\.\S+$/.test(value),
},
{
key: 'country',
isValid: (value) => value !== '',
},
{
key: 'agreedToTerms',
isValid: (value) => value === true,
},
];
const totalFields = fields.length;
let completedFields = 0;
fields.forEach(field => {
if (field.isValid(formData[field.key])) {
completedFields++;
}
});
return Math.round((completedFields / totalFields) * 100);
}, [formData]);
Este hook useMemo agora nos dá uma variável completionPercentage que estará sempre atualizada com o status de conclusão do formulário.
Passo 3: Criando a UI da Barra de Progresso Dinâmica
Vamos criar um componente reutilizável ProgressBar. Ele receberá a porcentagem calculada como uma prop e a exibirá visualmente.
// ProgressBar.js
import React from 'react';
export default function ProgressBar({ percentage }) {
return (
<div className="progress-container">
<div className="progress-bar" style={{ width: `${percentage}%` }}>
<span className="progress-label">{percentage}% Completo</span>
</div>
</div>
);
}
E aqui está um pouco de CSS básico para deixá-lo com uma boa aparência. Você pode adicionar isso à sua folha de estilos global.
/* styles.css */
.progress-container {
width: 100%;
background-color: #e0e0e0;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
}
.progress-bar {
height: 24px;
background-color: #4CAF50; /* Uma cor verde agradável */
text-align: right;
color: white;
display: flex;
align-items: center;
justify-content: center;
transition: width 0.5s ease-in-out;
}
.progress-label {
padding: 5px;
font-weight: bold;
font-size: 14px;
}
Passo 4: Integrando Tudo
Agora, vamos importar e usar nossa ProgressBar no componente principal RegistrationForm.
// Em RegistrationForm.js
import ProgressBar from './ProgressBar'; // Ajuste o caminho de importação
// ... (dentro da declaração de retorno de RegistrationForm)
return (
<form className="form-container">
<h2>Crie Sua Conta</h2>
<ProgressBar percentage={completionPercentage} />
{/* ... resto dos campos do formulário ... */}
</form>
);
Com isso implementado, ao preencher o formulário, você verá a barra de progresso animar suavemente de 0% a 100%. Resolvemos com sucesso a primeira metade do nosso problema: fornecer feedback em tempo real sobre a conclusão do formulário.
Onde o useFormStatus se Encaixa: Aprimorando a Experiência de Envio
O formulário está 100% completo, a barra de progresso está cheia e o usuário clica em "Enviar". O que acontece agora? É aqui que o useFormStatus brilha, permitindo-nos fornecer um feedback claro durante o processo de envio de dados.
Primeiro, vamos definir uma Server Action que cuidará do envio do nosso formulário. Para este exemplo, ela apenas simulará um atraso de rede.
// Em um novo arquivo, ex: 'actions.js'
'use server';
// Simula um atraso de rede e processa os dados do formulário
export async function createUser(formData) {
console.log('Server Action recebida:', formData.get('fullName'));
// Simula uma chamada ao banco de dados ou outra operação assíncrona
await new Promise(resolve => setTimeout(resolve, 2000));
// Em uma aplicação real, você lidaria com estados de sucesso/erro
console.log('Criação de usuário bem-sucedida!');
// Você poderia redirecionar o usuário ou retornar uma mensagem de sucesso
}
Em seguida, criamos um componente SubmitButton dedicado. Lembre-se da regra: o useFormStatus deve estar em um componente filho do formulário.
// SubmitButton.js
'use client';
import { useFormStatus } from 'react-dom';
export default function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Criando Conta...' : 'Criar Conta'}
</button>
);
}
Este componente simples faz muito. Ele se inscreve automaticamente no estado do formulário. Quando um envio está em andamento (pending é true), ele se desabilita para evitar múltiplos envios e muda seu texto para informar ao usuário que algo está acontecendo.
Finalmente, atualizamos nosso RegistrationForm para usar a Server Action e nosso novo SubmitButton.
// Em RegistrationForm.js
import { createUser } from './actions'; // Importa a server action
import SubmitButton from './SubmitButton'; // Importa o botão
// ...
export default function RegistrationForm() {
// ... (todo o estado e lógica existentes)
return (
// Passa a server action para a prop 'action' do formulário
<form className="form-container" action={createUser}>
<h2>Crie Sua Conta</h2>
<ProgressBar percentage={completionPercentage} />
{/* Todos os campos do formulário permanecem os mesmos */}
{/* Nota: O atributo 'name' em cada input é crucial */}
{/* para que as Server Actions criem o objeto FormData. */}
<div className="form-group">
<label htmlFor="fullName">Nome Completo</label>
<input name="fullName" ... />
</div>
{/* ... outros inputs com atributos 'name' ... */}
<SubmitButton />
</form>
);
}
Agora temos um formulário completo e moderno. A barra de progresso guia o usuário enquanto ele o preenche, e o botão de envio fornece um feedback claro e inequívoco durante o processo de submissão. Essa sinergia entre o estado do lado do cliente e o useFormStatus cria uma experiência de usuário robusta e profissional.
Conceitos Avançados e Melhores Práticas
Lidando com Validação Complexa com Bibliotecas
Para formulários mais complexos, escrever a lógica de validação manualmente pode se tornar tedioso. Bibliotecas como Zod ou Yup permitem que você defina um esquema para seus dados, que pode então ser usado para validação.
Você pode integrar isso ao nosso cálculo de conclusão. Em vez de uma função isValid personalizada para cada campo, você poderia tentar analisar cada campo em relação à sua definição de esquema e contar os sucessos.
// Exemplo usando Zod (conceitual)
import { z } from 'zod';
const userSchema = z.object({
fullName: z.string().min(1, 'O nome é obrigatório'),
email: z.string().email(),
country: z.string().min(1, 'O país é obrigatório'),
agreedToTerms: z.literal(true, { message: 'Você deve concordar com os termos' }),
});
// No seu cálculo com useMemo:
const completedFields = Object.keys(formData).reduce((count, key) => {
const fieldSchema = userSchema.shape[key];
const result = fieldSchema.safeParse(formData[key]);
return result.success ? count + 1 : count;
}, 0);
Considerações de Acessibilidade (a11y)
Uma ótima experiência do usuário é uma experiência acessível. Nosso indicador de progresso deve ser compreensível para usuários de tecnologias assistivas, como leitores de tela.
Aprimore o componente ProgressBar com atributos ARIA:
// ProgressBar Aprimorado.js
export default function ProgressBar({ percentage }) {
return (
<div
role="progressbar"
aria-valuenow={percentage}
aria-valuemin="0"
aria-valuemax="100"
aria-label={`Conclusão do formulário: ${percentage} por cento`}
className="progress-container"
>
{/* ... div interna ... */}
</div>
);
}
role="progressbar": Informa à tecnologia assistiva que este elemento é uma barra de progresso.aria-valuenow: Comunica o valor atual.aria-valueminearia-valuemax: Definem o intervalo.aria-label: Fornece uma descrição legível por humanos sobre o progresso.
Armadilhas Comuns e Como Evitá-las
- Usar `useFormStatus` no Lugar Errado: O erro mais comum. Lembre-se, o componente que usa este hook deve ser um filho do
<form>. Encapsular seu botão de envio em seu próprio componente é o padrão correto. - Esquecer os Atributos `name` nos Inputs: Ao usar Server Actions, o atributo
namenão é negociável. É como o React constrói o objetoFormDataque é enviado para o servidor. Sem ele, sua server action não receberá dados. - Confundir Validação do Cliente e do Servidor: O percentual de conclusão em tempo real é baseado na validação do lado do cliente para feedback imediato de UX. Você deve sempre revalidar os dados no servidor dentro da sua Server Action. Nunca confie nos dados vindos do cliente.
Conclusão
Desconstruímos com sucesso o processo de construção de um formulário sofisticado e amigável ao usuário no React moderno. Ao entender os papéis distintos do estado do lado do cliente e do hook useFormStatus, podemos criar experiências que guiam os usuários, fornecem feedback claro e, em última análise, aumentam as taxas de conversão.
Aqui estão os pontos principais:
- Para Feedback em Tempo Real (pré-envio): Use o gerenciamento de estado do lado do cliente (
useState) para rastrear as mudanças nos inputs e calcular o progresso da conclusão. UseuseMemopara otimizar esses cálculos. - Para Feedback de Envio (durante/pós-envio): Use o hook
useFormStatusdentro de um componente filho do seu formulário para gerenciar a UI durante o estado pendente (ex: desabilitar botões, mostrar spinners). - Sinergia é a Chave: A combinação dessas duas abordagens cobre todo o ciclo de vida da interação de um usuário com um formulário, do início ao fim.
- Sempre Priorize a Acessibilidade: Use atributos ARIA para garantir que seus componentes dinâmicos sejam utilizáveis por todos.
Ao implementar esses padrões, você vai além de simplesmente coletar dados e começa a projetar uma conversa com seus usuários—uma que seja clara, encorajadora e respeitosa com o tempo e o esforço deles.